/*
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 * AddID.java
 * Copyright (C) 2005 University of Waikato, Hamilton, New Zealand
 */

package weka.filters.unsupervised.attribute;

import weka.core.Attribute;
import weka.core.Capabilities;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Option;
import weka.core.OptionHandler;
import weka.core.RevisionUtils;
import weka.core.SingleIndex;
import weka.core.Utils;
import weka.core.Capabilities.Capability;
import weka.filters.Filter;
import weka.filters.UnsupervisedFilter;

import java.util.Enumeration;
import java.util.Vector;

/**
 * <!-- globalinfo-start --> An instance filter that adds an ID attribute to the
 * dataset. The new attribute contains a unique ID for each instance.<br/>
 * Note: The ID is not reset for the second batch of files (using -b and -r and
 * -s).
 * <p/>
 * <!-- globalinfo-end -->
 * 
 * <!-- options-start --> Valid options are:
 * <p/>
 * 
 * <pre>
 * -C &lt;index&gt;
 *  Specify where to insert the ID. First and last
 *  are valid indexes.(default first)
 * </pre>
 * 
 * <pre>
 * -N &lt;name&gt;
 *  Name of the new attribute.
 *  (default = 'ID')
 * </pre>
 * 
 * <!-- options-end -->
 * 
 * @author fracpete (fracpete at waikato dot ac dot nz)
 * @version $Revision: 5543 $
 */
public class AddID extends Filter implements UnsupervisedFilter, OptionHandler {

	/** for serialization */
	static final long serialVersionUID = 4734383199819293390L;

	/** the index of the attribute */
	protected SingleIndex m_Index = new SingleIndex("first");

	/** the name of the attribute */
	protected String m_Name = "ID";

	/** the counter for the ID */
	protected int m_Counter = -1;

	/**
	 * Returns a string describing this filter
	 * 
	 * @return a description of the filter suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String globalInfo() {
		return "An instance filter that adds an ID attribute to the dataset. "
				+ "The new attribute contains a unique ID for each instance.\n"
				+ "Note: The ID is not reset for the second batch of files (using -b "
				+ "and -r and -s).";
	}

	/**
	 * Returns an enumeration describing the available options.
	 * 
	 * @return an enumeration of all the available options.
	 */
	public Enumeration listOptions() {
		Vector result = new Vector();

		result.addElement(new Option(
				"\tSpecify where to insert the ID. First and last\n"
						+ "\tare valid indexes.(default first)", "C", 1,
				"-C <index>"));

		result.addElement(new Option("\tName of the new attribute.\n"
				+ "\t(default = 'ID')", "N", 1, "-N <name>"));

		return result.elements();
	}

	/**
	 * Parses a given list of options.
	 * <p/>
	 * 
	 * <!-- options-start --> Valid options are:
	 * <p/>
	 * 
	 * <pre>
	 * -C &lt;index&gt;
	 *  Specify where to insert the ID. First and last
	 *  are valid indexes.(default first)
	 * </pre>
	 * 
	 * <pre>
	 * -N &lt;name&gt;
	 *  Name of the new attribute.
	 *  (default = 'ID')
	 * </pre>
	 * 
	 * <!-- options-end -->
	 * 
	 * @param options
	 *            the list of options as an array of strings
	 * @throws Exception
	 *             if an option is not supported
	 */
	public void setOptions(String[] options) throws Exception {
		String tmpStr;

		tmpStr = Utils.getOption('C', options);
		if (tmpStr.length() != 0)
			m_Index.setSingleIndex(tmpStr);
		else
			m_Index.setSingleIndex("first");

		tmpStr = Utils.getOption('N', options);
		if (tmpStr.length() != 0)
			m_Name = tmpStr;
		else
			m_Name = "ID";

		if (getInputFormat() != null)
			setInputFormat(getInputFormat());
	}

	/**
	 * Gets the current settings of the filter.
	 * 
	 * @return an array of strings suitable for passing to setOptions
	 */
	public String[] getOptions() {
		Vector result;

		result = new Vector();

		result.add("-C");
		result.add(getIDIndex());

		result.add("-N");
		result.add(getAttributeName());

		return (String[]) result.toArray(new String[result.size()]);
	}

	/**
	 * Returns the tip text for this property
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String attributeNameTipText() {
		return "Set the new attribute's name.";
	}

	/**
	 * Get the name of the attribute to be created
	 * 
	 * @return the current attribute name
	 */
	public String getAttributeName() {
		return m_Name;
	}

	/**
	 * Set the new attribute's name
	 * 
	 * @param value
	 *            the new name
	 */
	public void setAttributeName(String value) {
		m_Name = value;
	}

	/**
	 * Returns the tip text for this property
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String IDIndexTipText() {
		return "The position (starting from 1) where the attribute will be inserted "
				+ "(first and last are valid indices).";
	}

	/**
	 * Get the index of the attribute used.
	 * 
	 * @return the index of the attribute
	 */
	public String getIDIndex() {
		return m_Index.getSingleIndex();
	}

	/**
	 * Sets index of the attribute used.
	 * 
	 * @param value
	 *            the index of the attribute
	 */
	public void setIDIndex(String value) {
		m_Index.setSingleIndex(value);
	}

	/**
	 * Returns the Capabilities of this filter.
	 * 
	 * @return the capabilities of this object
	 * @see Capabilities
	 */
	public Capabilities getCapabilities() {
		Capabilities result = super.getCapabilities();
		result.disableAll();

		// attributes
		result.enableAllAttributes();
		result.enable(Capability.MISSING_VALUES);

		// class
		result.enableAllClasses();
		result.enable(Capability.MISSING_CLASS_VALUES);
		result.enable(Capability.NO_CLASS);

		return result;
	}

	/**
	 * Sets the format of the input instances.
	 * 
	 * @param instanceInfo
	 *            an Instances object containing the input instance structure
	 *            (any instances contained in the object are ignored - only the
	 *            structure is required).
	 * @return true if the outputFormat may be collected immediately
	 * @throws Exception
	 *             if the format couldn't be set successfully
	 */
	public boolean setInputFormat(Instances instanceInfo) throws Exception {
		Instances outputFormat;
		Attribute newAttribute;

		super.setInputFormat(instanceInfo);

		m_Counter = -1;
		m_Index.setUpper(instanceInfo.numAttributes());
		outputFormat = new Instances(instanceInfo, 0);
		newAttribute = new Attribute(m_Name);

		if ((m_Index.getIndex() < 0)
				|| (m_Index.getIndex() > getInputFormat().numAttributes()))
			throw new IllegalArgumentException("Index out of range");

		outputFormat.insertAttributeAt(newAttribute, m_Index.getIndex());
		setOutputFormat(outputFormat);

		return true;
	}

	/**
	 * Input an instance for filtering. Filter requires all training instances
	 * be read before producing output.
	 * 
	 * @param instance
	 *            the input instance
	 * @return true if the filtered instance may now be collected with output().
	 * @throws IllegalStateException
	 *             if no input format has been set.
	 */
	public boolean input(Instance instance) {
		if (getInputFormat() == null)
			throw new IllegalStateException("No input instance format defined");

		if (m_NewBatch) {
			resetQueue();
			m_NewBatch = false;
		}

		if (!isFirstBatchDone()) {
			bufferInput(instance);
			return false;
		} else {
			convertInstance(instance);
			return true;
		}
	}

	/**
	 * Signify that this batch of input to the filter is finished. If the filter
	 * requires all instances prior to filtering, output() may now be called to
	 * retrieve the filtered instances.
	 * 
	 * @return true if there are instances pending output
	 * @throws IllegalStateException
	 *             if no input structure has been defined
	 */
	public boolean batchFinished() {
		if (getInputFormat() == null)
			throw new IllegalStateException("No input instance format defined");

		if (!isFirstBatchDone()) {
			m_Counter = 0;

			// Convert pending input instances
			for (int i = 0; i < getInputFormat().numInstances(); i++)
				convertInstance(getInputFormat().instance(i));
		}

		// Free memory
		flushInput();

		m_NewBatch = true;
		m_FirstBatchDone = true;

		return (numPendingOutput() != 0);
	}

	/**
	 * Convert a single instance over. The converted instance is added to the
	 * end of the output queue.
	 * 
	 * @param instance
	 *            the instance to convert
	 */
	protected void convertInstance(Instance instance) {
		Instance inst;

		m_Counter++;

		// build instance
		try {
			inst = (Instance) instance.copy();

			// First copy string values from input to output
			copyValues(inst, true, inst.dataset(), getOutputFormat());

			// Insert the new attribute and reassign to output
			inst.setDataset(null);
			inst.insertAttributeAt(m_Index.getIndex());
			inst.setValue(m_Index.getIndex(), m_Counter);
			inst.setDataset(getOutputFormat());

			push(inst);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * Returns the revision string.
	 * 
	 * @return the revision
	 */
	public String getRevision() {
		return RevisionUtils.extract("$Revision: 5543 $");
	}

	/**
	 * Main method for testing this class.
	 * 
	 * @param args
	 *            should contain arguments to the filter: use -h for help
	 */
	public static void main(String[] args) {
		runFilter(new AddID(), args);
	}
}
